﻿using Bot.Dynamics365.Enum;
using Bot.Dynamics365.Extension;
using Bot.Dynamics365.Model;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Polly;
using Polly.Caching;
using Polly.Caching.Memory;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

namespace Bot.Dynamics365
{
    public class WebRequestService : IWebRequestService
    {
        private readonly ITokenService _tokenService;
        private readonly IHttpClientFactory _httpClientFactory;
        private readonly ILogger<WebRequestService> _logger;
        private readonly IAsyncCacheProvider _memoryCacheProvider;
        private readonly IMemoryCache _memoryCache;

        public WebRequestService(ITokenService tokenService,
            IHttpClientFactory httpClientFactory,
            ILogger<WebRequestService> logger,
            IAsyncCacheProvider memoryCacheProvider,
            IMemoryCache memoryCache)
        {
            _tokenService = tokenService;
            _httpClientFactory = httpClientFactory;
            _logger = logger;
            _memoryCacheProvider = memoryCacheProvider;
            _memoryCache = memoryCache;
        }

        public async Task<HttpResponse> SendAsync(AzureAD azure, string url, EnumHttpMethod method, string content = "", Dictionary<string,string> headers = null)
        {
            var input = JsonConvert.SerializeObject(new
            {
                azure,
                method = method.ToString(),
                content,
                url = Utils.ConnectUrl(azure.InstanceUrl, url),
            });
            _logger.LogDebug($"SendAsync input:{input}");

            var response = default(HttpResponse);
            switch (method)
            {
                case EnumHttpMethod.Get:
                case EnumHttpMethod.Post:
                case EnumHttpMethod.Patch:
                    response = await SendAsyncRetry(azure, url, method, content, headers);
                    break;
                default:
                    throw new Exception("method type invalid");
            }

            _logger.LogDebug($"SendAsync response:{ JsonConvert.SerializeObject(response)}");

            return response;
        }

        private async Task<string> TokenAsyncByCache(AzureAD azure, bool forceRefresh = false)
        {
            AsyncCachePolicy policy = Policy.CacheAsync(_memoryCacheProvider, ttl: TimeSpan.FromMinutes(30));
            Context policyExecutionContext = new Context($"GetToken-By-Host-Dynamics365-{azure.InstanceUrl}");

            if (forceRefresh)
            {
                _memoryCache.Remove(policyExecutionContext.OperationKey);
            }

            var token = await policy.ExecuteAsync(context => _tokenService.GetAccessTokenAsync(azure), policyExecutionContext);

            return token;
        }

        private bool IsAuthorization(HttpResponse response)
        {
            if (response != null
                && response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
            {
                return false;
            }

            return true;
        }

        private HttpResponse JudgeResponse(HttpResponse response)
        {
            if (!IsAuthorization(response))
            {
                throw new UnAuthorizationException("invalid token");
            }

            return response;
        }

        private async Task<HttpResponse> SendAsyncRetry(AzureAD azure, string url, EnumHttpMethod method, string content = "", Dictionary<string, string> headers = null)
        {
            var token = await TokenAsyncByCache(azure);
            var retryTimes = 1;
            var policy = Policy.Handle<UnAuthorizationException>()
                .RetryAsync(retryTimes, async (ex, onRetry) =>
                {
                    token = await TokenAsyncByCache(azure, true);
                });

            HttpResponse response = await policy.ExecuteAsync(async () =>
            {
                var request = Utils.BuildHttpRequestMessage(azure.InstanceUrl, url, method)
                .AddAuthorization(token)
                .AddHeaders(headers);

                if (method == EnumHttpMethod.Post || method == EnumHttpMethod.Patch)
                {
                    request.AddContent(content);
                }

                return JudgeResponse(await _httpClientFactory.SendAsync(request));
            });

            return response;
        }
    }
}
